#include "degfx/bitmap.h"

#define MXMODULE_FGETW
#define MXMODULE_BITPIXEL

#include <stdio.h>
#include <string.h>

typedef struct MX__GIFRGB {
	unsigned char r;
	unsigned char g;
	unsigned char b;
} MX__GIFRGB;

typedef MX__GIFRGB MX__GIFPALETTE[256];

typedef struct MX__GIFFRAME {
	MX_BITMAP *bitmap_8_bit;
	MX__GIFPALETTE palette;
	int xoff, yoff;
	int duration;
	int disposal;
} MX__GIFFRAME;

typedef struct MX__GIFANIMATION {
	int width, height;
	int frame_count;
	int background_color;
	MX__GIFFRAME *frames;
	int transparent;
} MX__GIFANIMATION;

static int mx__gif_readcode(FILE * file, char *buf, int *bit_pos, int bit_size)
{
	int i;
	int code = 0;
	int pos = 1;

	for (i = 0; i < bit_size; i++) {
		int byte_pos = (*bit_pos >> 3) & 255;

		if (byte_pos == 0) {
			int data_len = getc(file);

			if (data_len == 0) {
				printf("Fatal. Errorneous GIF stream.\n");
				abort();
			}

			fread(buf + 256 - data_len, 1, data_len, file);
			byte_pos = 256 - data_len;
			*bit_pos = byte_pos << 3;
		}
		if (buf[byte_pos] & (1 << (*bit_pos & 7)))
			code += pos;

		pos += pos;
		(*bit_pos)++;
	}
	return code;
}

static void mx__gif_writepixel(MX_BITMAP * bmp, int pos, int code)
{
	const int w = mx_w(bmp) + 1;

	mx_bitmap_pixel(bmp, pos % w, pos / w, code);
}

static void mx__gif_LZWdecode(FILE * file, MX_BITMAP * bmp)
{
	int orig_bit_size;
	char buf[256];
	int bit_size;
	int bit_pos;
	int clear_marker;
	int end_marker;
	struct {
		int prefix;
		int c;
		int len;
	} codes[4096];				/* Maximum bit size is 12. */
	int n;
	int i, prev, code, c;
	int out_pos = 0;

	orig_bit_size = getc(file);
	n = 2 + (1 << orig_bit_size);

	for (i = 0; i < n; i++) {
		codes[i].c = i;
		codes[i].len = 0;
	}

	clear_marker = n - 2;
	end_marker = n - 1;

	bit_size = orig_bit_size + 1;

	bit_pos = 0;

	/* Expect to read clear code as first code here. */
	prev = mx__gif_readcode(file, buf, &bit_pos, bit_size);

	do {
		code = mx__gif_readcode(file, buf, &bit_pos, bit_size);

		if (code == clear_marker) {
			bit_size = orig_bit_size;
			n = 1 << bit_size;
			n += 2;
			bit_size++;
			prev = code;
			continue;
		}

		if (code == end_marker)
			break;

		/* Known code: ok. Else: must be doubled char. */
		if (code < n)
			c = code;
		else
			c = prev;

		/* Output the code. */
		out_pos += codes[c].len;
		i = 0;
		do {
			mx__gif_writepixel(bmp, out_pos - i, codes[c].c);
			if (codes[c].len)
				c = codes[c].prefix;
			else
				break;
			i++;
		}
		while (1);

		out_pos++;

		/* Unknown code -> must be double char. */
		if (code >= n) {
			mx__gif_writepixel(bmp, out_pos, codes[c].c);
			out_pos++;
		}

		/* Except after clear marker, build new code. */
		if (prev != clear_marker) {
			codes[n].prefix = prev;
			codes[n].len = codes[prev].len + 1;
			codes[n].c = codes[c].c;
			n++;
		}

		/* Out of bits? Increase. */
		if (n == (1 << bit_size)) {
			if (bit_size < 12)
				bit_size++;
		}

		prev = code;
	}
	while (1);
}

/* Destroy a complete gif, including all frames. */
static void mx__gif_destroyanimation(MX__GIFANIMATION * gif)
{
	int i;

	for (i = 0; i < gif->frame_count; i++) {
		MX__GIFFRAME *frame = gif->frames + i;

		if (frame->bitmap_8_bit)
			mx_delete(frame->bitmap_8_bit);
	}
	mx_free(gif->frames);
	mx_free(gif);
}

/**
 * Allocates and reads a GIF_ANIMATION structure, filling in all the
 * frames found in the file. On error, nothing is allocated, and 0 is
 * returned. No extensions or comments are read in. If the gif contains
 * a transparency index, and it is not 0, it is swapped with 0 - so index
 * 0 will be the transparent color. There is no way to know when a file
 * contained no transparency originally. Frame duration is specified in
 * 1/100th seconds.
 */
static MX__GIFANIMATION *mx__gif_loadanimation(const char *filename)
{
	int depth;
	int version;
	int transparent = -1;
	MX_BITMAP *bmp = 0;
	int i;
	MX__GIFANIMATION *gif = mx_malloc(sizeof(*gif));
	MX__GIFFRAME frame;
	FILE *file;
	MX__GIFPALETTE global_palette;
	int have_global_palette = 0;

	memset(gif, 0, sizeof(*gif));

	gif->frame_count = 0;
	file = fopen(filename, "rb");
	if (!file)
		goto error;

	/* is it really a GIF? */
	if (getc(file) != 'G')
		goto error;
	if (getc(file) != 'I')
		goto error;
	if (getc(file) != 'F')
		goto error;
	if (getc(file) != '8')
		goto error;
	/* '7' or '9', for 87a or 89a. */
	version = getc(file);
	if (version != '7' && version != '9')
		goto error;
	if (getc(file) != 'a')
		goto error;

	gif->width = mx_fgetw(file);
	gif->height = mx_fgetw(file);
	i = getc(file);
	/* Global colour table? */
	if (i & 128)
		depth = (i & 7) + 1;
	else
		depth = 0;
	gif->background_color = getc(file);

	/* Skip aspect ratio. */
	fseek(file, 1, SEEK_CUR);
	if (depth) {
		for (i = 0; i < (1 << depth); i++) {
			global_palette[i].r = getc(file);
			global_palette[i].g = getc(file);
			global_palette[i].b = getc(file);
		}
		have_global_palette = 1;
	}

	do {
		i = getc(file);

		switch (i) {
		case 0x2c:
			{					/* Image Descriptor */
				int w, h;

				frame.xoff = mx_fgetw(file);
				frame.yoff = mx_fgetw(file);
				w = mx_fgetw(file);
				h = mx_fgetw(file);
				bmp = mx_bitmap(w - 1, h - 1);
				if (!bmp)
					goto error;
				mx_bitmap_clear(bmp, 0);

				i = getc(file);

				/* Local palette. */
				if (i & 128) {
					depth = (i & 7) + 1;

					for (i = 0; i < (1 << depth); i++) {
						frame.palette[i].r = getc(file);
						frame.palette[i].g = getc(file);
						frame.palette[i].b = getc(file);
					}
				} else {
					if (have_global_palette)
						memcpy(frame.palette, global_palette, sizeof(MX__GIFPALETTE));
				}

				mx__gif_LZWdecode(file, bmp);

				if (transparent != -1) {
					int x, y;
					MX__GIFRGB rgb;

					for (y = 0; y < mx_h(bmp) + 1; y++) {
						for (x = 0; x < mx_w(bmp) + 1; x++) {
							int c = mx_bitmap_getpixel(bmp, x, y);

							if (c == 0)
								mx_bitmap_pixel(bmp, x, y, transparent);
							else if (c == transparent)
								mx_bitmap_pixel(bmp, x, y, 0);
						}
					}

					rgb = frame.palette[transparent];

					frame.palette[transparent] = frame.palette[0];
					frame.palette[0] = rgb;
				}
				frame.bitmap_8_bit = bmp;
				bmp = 0;

				gif->frame_count++;
				if (gif->frames)
					gif->frames = mx_realloc(gif->frames, gif->frame_count * sizeof(*gif->frames));
				else
					gif->frames = mx_malloc(sizeof(*gif->frames));

				gif->frames[gif->frame_count - 1] = frame;
				transparent = -1;

				break;
			}
		case 0x21:
			/* Extension Introducer. */
			i = getc(file);
			/* Graphic Control Extension. */
			if (i == 0xf9) {
				/* size must be 4 */
				if (getc(file) != 4)
					goto error;
				i = getc(file);
				frame.disposal = (i >> 2) & 7;
				frame.duration = mx_fgetw(file);

				/* Transparency? */
				if (i & 1) {
					transparent = getc(file);
					gif->transparent = 1;
				} else
					getc(file);
			}
			/* Possibly more blocks until terminator block. */
			i = getc(file);
			while (i) {
				fseek(file, i, SEEK_CUR);
				i = getc(file);
			}
			break;
		case 0x3b:
			/* GIF Trailer. */
			fclose(file);
			return gif;
		}

	}
	while (true);

  error:
	if (file)
		fclose(file);
	if (gif)
		mx__gif_destroyanimation(gif);
	if (bmp)
		mx_delete(bmp);
	return 0;
}

MX_BITMAP *mx_bitmap_gif(const char *filename)
{
	MX_BITMAP *bmp = 0;
	MX__GIFANIMATION *gif = mx__gif_loadanimation(filename);

	if (gif) {
		int i;
		int x = 0;

		bmp = mx_bitmap(gif->frame_count * (gif->width - 1), gif->height - 1);
		if (!bmp)
			goto done;

		MXDEBUG_ALLOCTAG(bmp);
		mx_bitmap_clear(bmp, 0);

		for (i = 0; i < gif->frame_count; i++) {
			const MX_BITMAP *frame = gif->frames[i].bitmap_8_bit;
			MX_BITMAP_ITER ptr = mx_bitmap_begin(frame);
			const MX_BITMAP_ITER end = mx_bitmap_end(frame);

			while (ptr != end) {
				const MX__GIFRGB entry = gif->frames[i].palette[*ptr];
				MX_PIXEL color = MXRGB(entry.r, entry.g, entry.b);

				if ((gif->transparent) && (*ptr == 0))
					color = MXRGBT(0, 0, 0, 0xFF);

				*ptr++ = color;
			}
			mx_bitmap_blit(frame, bmp, 0, 0, gif->frames[i].xoff, gif->frames[i].yoff, MXDEFAULT, MXDEFAULT);

			x += gif->width;
		}
	  done:

		mx__gif_destroyanimation(gif);
	}
	return bmp;
}
